namespace GameFrame.Deterministic
{
  /// <summary>
  /// Represents 2x2 column major matrix, which can be used for 2D scaling and rotation.
  /// Each cell can be individually accessed as a field (M&lt;row&gt;&lt;column&gt;).
  /// </summary>
  /// 
  ///             \ingroup MathAPI
  [Serializable]
  [StructLayout(LayoutKind.Explicit)]
  public struct FPMatrix2x2
  {
    public const int SIZE = 32;
    [FieldOffset(0)]
    public FP M00;
    [FieldOffset(8)]
    public FP M10;
    [FieldOffset(16)]
    public FP M01;
    [FieldOffset(24)]
    public FP M11;

    /// <summary>Adds two matrices.</summary>
    public static FPMatrix2x2 operator +(FPMatrix2x2 a, FPMatrix2x2 b)
    {
      a.M00.RawValue += b.M00.RawValue;
      a.M01.RawValue += b.M01.RawValue;
      a.M10.RawValue += b.M10.RawValue;
      a.M11.RawValue += b.M11.RawValue;
      return a;
    }

    /// <summary>Subtracts two matrices.</summary>
    public static FPMatrix2x2 operator -(FPMatrix2x2 a, FPMatrix2x2 b)
    {
      a.M00.RawValue -= b.M00.RawValue;
      a.M01.RawValue -= b.M01.RawValue;
      a.M10.RawValue -= b.M10.RawValue;
      a.M11.RawValue -= b.M11.RawValue;
      return a;
    }

    /// <summary>Multiplies two matrices.</summary>
    public static FPMatrix2x2 operator *(FPMatrix2x2 a, FPMatrix2x2 b)
    {
      FPMatrix2x2 fpMatrix2x2;
      fpMatrix2x2.M00.RawValue = (a.M00.RawValue * b.M00.RawValue >> 16) + (a.M01.RawValue * b.M10.RawValue >> 16);
      fpMatrix2x2.M01.RawValue = (a.M00.RawValue * b.M01.RawValue >> 16) + (a.M01.RawValue * b.M11.RawValue >> 16);
      fpMatrix2x2.M10.RawValue = (a.M10.RawValue * b.M00.RawValue >> 16) + (a.M11.RawValue * b.M10.RawValue >> 16);
      fpMatrix2x2.M11.RawValue = (a.M10.RawValue * b.M01.RawValue >> 16) + (a.M11.RawValue * b.M11.RawValue >> 16);
      return fpMatrix2x2;
    }

    /// <summary>Multiplies a vector by a matrix.</summary>
    public static FPVector2 operator *(FPMatrix2x2 m, FPVector2 vector)
    {
      FPVector2 fpVector2;
      fpVector2.X.RawValue = (m.M00.RawValue * vector.X.RawValue >> 16) + (m.M01.RawValue * vector.Y.RawValue >> 16);
      fpVector2.Y.RawValue = (m.M10.RawValue * vector.X.RawValue >> 16) + (m.M11.RawValue * vector.Y.RawValue >> 16);
      return fpVector2;
    }

    /// <summary>Multiplies a matrix by a factor.</summary>
    public static FPMatrix2x2 operator *(FP a, FPMatrix2x2 m)
    {
      m.M00.RawValue = a.RawValue * m.M00.RawValue >> 16;
      m.M01.RawValue = a.RawValue * m.M01.RawValue >> 16;
      m.M10.RawValue = a.RawValue * m.M10.RawValue >> 16;
      m.M11.RawValue = a.RawValue * m.M11.RawValue >> 16;
      return m;
    }

    /// <summary>Attempts to get a scale value from the matrix.</summary>
    public FPVector2 LossyScale
    {
      get
      {
        long x1 = (this.M00.RawValue * this.M00.RawValue >> 16) + (this.M10.RawValue * this.M10.RawValue >> 16);
        long x2 = (this.M01.RawValue * this.M01.RawValue >> 16) + (this.M11.RawValue * this.M11.RawValue >> 16);
        return new FPVector2(FP.FromRaw(FPMath.SqrtRaw(x1) * (long) FPMath.SignInt(this.Determinant)), FP.FromRaw(FPMath.SqrtRaw(x2)));
      }
    }

    /// <summary>
    /// Creates inverted matrix. Matrix with determinant 0 can not be inverted and result with <see cref="P:Photon.Deterministic.FPMatrix2x2.Zero" />.
    /// </summary>
    public FPMatrix2x2 Inverted
    {
      get
      {
        long num1 = (this.M00.RawValue * this.M11.RawValue >> 16) - (this.M10.RawValue * this.M01.RawValue >> 16);
        if (num1 == 0L)
          return FPMatrix2x2.Zero;
        long num2 = 4294967296L / num1;
        FPMatrix2x2 inverted;
        inverted.M00.RawValue = this.M11.RawValue * num2 >> 16;
        inverted.M01.RawValue = -(this.M01.RawValue * num2 >> 16);
        inverted.M10.RawValue = -(this.M10.RawValue * num2 >> 16);
        inverted.M11.RawValue = this.M00.RawValue * num2 >> 16;
        return inverted;
      }
    }

    /// <summary>Calculates determinant of this matrix.</summary>
    public FP Determinant => FP.FromRaw((this.M00.RawValue * this.M11.RawValue >> 16) - (this.M10.RawValue * this.M01.RawValue >> 16));

    /// <summary>Matrix with 0s in every cell.</summary>
    public static FPMatrix2x2 Zero => new FPMatrix2x2();

    /// <summary>
    /// Matrix with 1s in the main diagonal and 0s in all other cells.
    /// </summary>
    public static FPMatrix2x2 Identity => new FPMatrix2x2()
    {
      M00 = {
        RawValue = 65536
      },
      M11 = {
        RawValue = 65536
      }
    };

    /// <summary>
    /// Create from columns - first two values set the first row, second two values - second row.
    /// </summary>
    public static FPMatrix2x2 FromRows(FP m00, FP m01, FP m10, FP m11)
    {
      FPMatrix2x2 fpMatrix2x2;
      fpMatrix2x2.M00 = m00;
      fpMatrix2x2.M10 = m10;
      fpMatrix2x2.M01 = m01;
      fpMatrix2x2.M11 = m11;
      return fpMatrix2x2;
    }

    /// <summary>
    /// Create from columns - first two values set the first colunn, second two values - second column.
    /// </summary>
    public static FPMatrix2x2 FromColumns(FP m00, FP m10, FP m01, FP m11)
    {
      FPMatrix2x2 fpMatrix2x2;
      fpMatrix2x2.M00 = m00;
      fpMatrix2x2.M10 = m10;
      fpMatrix2x2.M01 = m01;
      fpMatrix2x2.M11 = m11;
      return fpMatrix2x2;
    }

    /// <summary>Creates a rotation matrix.</summary>
    /// <param name="rotation">Rotation in radians.</param>
    public static FPMatrix2x2 Rotate(FP rotation)
    {
      FPMatrix2x2 fpMatrix2x2;
      FPMath.SinCos(rotation, out fpMatrix2x2.M01, out fpMatrix2x2.M00);
      fpMatrix2x2.M10 = -fpMatrix2x2.M01;
      fpMatrix2x2.M11 = fpMatrix2x2.M00;
      return fpMatrix2x2;
    }

    /// <summary>
    /// Returns true if this matrix is equal to the <see cref="P:Photon.Deterministic.FPMatrix2x2.Identity" /> matrix
    /// </summary>
    public bool IsIdentity => this.M00.RawValue == 65536L && this.M11.RawValue == 65536L && (this.M01.RawValue | this.M10.RawValue) == 0L;

    /// <summary>Gets or sets cell M&lt;index%4&gt;&lt;index/4&gt;</summary>
    /// <param name="index"></param>
    /// <returns></returns>
    public FP this[int index]
    {
      get
      {
        switch (index)
        {
          case 0:
            return this.M00;
          case 1:
            return this.M10;
          case 2:
            return this.M01;
          case 3:
            return this.M11;
          default:
            throw new ArgumentOutOfRangeException();
        }
      }
      set
      {
        switch (index)
        {
          case 0:
            this.M00 = value;
            break;
          case 1:
            this.M10 = value;
            break;
          case 2:
            this.M01 = value;
            break;
          case 3:
            this.M11 = value;
            break;
          default:
            throw new ArgumentOutOfRangeException();
        }
      }
    }

    /// <summary>Creates a scaling matrix.</summary>
    public static FPMatrix2x2 Scale(FPVector2 scale) => FPMatrix2x2.FromColumns(scale.X, (FP) 0, (FP) 0, scale.Y);

    /// <summary>Transforms a direction by this matrix.</summary>
    public FPVector2 MultiplyVector(FPVector2 v)
    {
      FPVector2 fpVector2;
      fpVector2.X.RawValue = (this.M00.RawValue * v.X.RawValue >> 16) + (this.M01.RawValue * v.Y.RawValue >> 16);
      fpVector2.Y.RawValue = (this.M10.RawValue * v.X.RawValue >> 16) + (this.M11.RawValue * v.Y.RawValue >> 16);
      return fpVector2;
    }

    public static unsafe void Serialize(void* ptr, IDeterministicFrameSerializer serializer)
    {
      FPMatrix2x2* fpMatrix2x2Ptr = (FPMatrix2x2*) ptr;
      FP.Serialize((void*) &fpMatrix2x2Ptr->M00, serializer);
      FP.Serialize((void*) &fpMatrix2x2Ptr->M10, serializer);
      FP.Serialize((void*) &fpMatrix2x2Ptr->M01, serializer);
      FP.Serialize((void*) &fpMatrix2x2Ptr->M11, serializer);
    }

    public override string ToString() => string.Format((IFormatProvider) CultureInfo.InvariantCulture, "(({0}, {1}), ({2}, {3}))", (object) this.M00.AsFloat, (object) this.M01.AsFloat, (object) this.M10.AsFloat, (object) this.M11.AsFloat);

    public override int GetHashCode() => (((17 * 31 + this.M00.GetHashCode()) * 31 + this.M10.GetHashCode()) * 31 + this.M01.GetHashCode()) * 31 + this.M11.GetHashCode();
  }
}